{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Contrarian view on mutable default arguments.\n",
    "\n",
    "The use of [mutable defaults](https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments) is probably the most infamous Python gotcha.  Default values are evaluated at definition time, which means mutating them will be persistent across multiple calls.  Many articles on this topic even use the same `append` example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": "[0]"
     },
     "metadata": {},
     "execution_count": 1
    }
   ],
   "source": [
    "def append_to(element, to=[]):\n",
    "    to.append(element)\n",
    "    return to\n",
    "\n",
    "append_to(0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": "[0, 1]"
     },
     "metadata": {},
     "execution_count": 2
    }
   ],
   "source": [
    "append_to(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And the solution is invariably to use `None` instead, and convert as needed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "def append_to(element, to=None):\n",
    "    if to is None:\n",
    "        to = []\n",
    "    to.append(element)\n",
    "    return to"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There is another solution to the problem of mutating a default value: don't do that.  More specifically, the problem isn't using mutables as defaults; the problem is actually mutating them.\n",
    "\n",
    "If the input from the caller is being mutated, then the caller doesn't need it returned because the caller already has a reference.  This distinction is explicitly encouraged in Python, e.g., `list.sort` vs. `sorted`.  But it follows that if the input doesn't need to be returned, then there's no point in the input being optional.  How would the caller know the difference?\n",
    "\n",
    "The reason why examples like the fluent `append` seem so contrived is because they are. No one actually wants a function named `append` to take one argument.  The realistic fix would be:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def append_to(element, to):\n",
    "    to.append(element)\n",
    "    return to"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sure, there are rare occassions where a parameter is mutable but optional, such as a recursive algorithm that's implicitly passing around its own cache.  But this author would wager that given any real-world code that's been bitten by this gotcha there is:\n",
    "* a ~90% chance the function would have a related bug if defaults were evaluated at runtime\n",
    "* a ~95% chance the function has a poor interface\n",
    "\n",
    "What harm does this advice do?  Well, it's caused an over-reaction resulting in using `None` as the only default, even for immutables.  It's so prevalent that it appears many beginners believe using `None` is the one and only way of making an argument optional.\n",
    "\n",
    "Besides immutable types, there are also cases where mutation is irrelevant.  Consider the following example adapted from a popular project."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import List\n",
    "\n",
    "def __init__(self, alist: List = None):\n",
    "    self.alist = [] if alist is None else list(alist)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that the correctness of this code relies on the member list being newly created in either case.  What could possibly go wrong with:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def __init__(self, alist: List = []):\n",
    "    self.alist = list(alist)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Or better yet, why not support the more liberal interface."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Iterable\n",
    "\n",
    "def __init__(self, alist: Iterable = ()):\n",
    "    self.alist = list(alist)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The point is that there are at least 4 solutions to this problem:\n",
    "\n",
    "1. use mutable defaults, but don't mutate them\n",
    "1. use immutable substitute defaults with a compatible interface\n",
    "1. use `None` for mutables, and matching types for immutables\n",
    "1. use `None` for all defaults\n",
    "\n",
    "Only #1 is even remotely controversial, yet somehow the status quo has landed on #4.  Besides being needlessly verbose, it has another pitfall.  Python doesn't natively support detecting where the argument was actually passed; a sentinel default is required for that.  The implementation detail is leaking through the interface, indicating to the caller that `None` is an acceptable argument to pass _explicitly_.  As if the type hint was `Optional[List]`, even though that's not the intention.  Factor in using `**kwargs` - which clearly doesn't want data padded with nulls - and actual code breakage can result.\n",
    "\n",
    "Presumably the disdain for option #1 is because it might _encourage_ the gotcha.  But it's disingenous to just let that go unsaid.  The implementer is responsible for writing correct code, and the caller sees the right interface.  The speculation is that beginners will read code which uses mutable defaults but doesn't mutate them, and follow the former pattern but not the latter.\n",
    "\n",
    "As a community, let's at least push towards option #3.  Using empty strings and zeros as defaults is all upside."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.8.3 64-bit",
   "env": {},
   "interrupt_mode": "signal",
   "language": "python",
   "metadata": {},
   "name": "python_defaultSpec_1593390719429"
  },
  "nikola": {
   "category": "style",
   "date": "2020-06-28",
   "description": "",
   "link": "",
   "slug": "mutable-defaults",
   "tags": "",
   "title": "Mutable defaults",
   "type": "text"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}